﻿package com.makinolo.rlvforge 
{
	import mx.validators.RegExpValidationResult;
	

public class GISPoint
{
	import mx.utils.StringUtil;
	
	public static const EARTH_RADIUS:Number = 6378137.0;	// This is the most used mean radius value
	//public static final int EARTH_RADIUS = 6378137;	// This is the most used mean radius value
	//public static final int EARTH_RADIUS = 6371900;		// With this value I get similar distance calcs
														// as CompeGPS	
	private var m_latitude:Number;
	private var m_longitude:Number;
	private var m_elevation:Number;
	private var m_time:Number;
	private var m_comment:String;
	private var m_commenttype:String;
	private var m_marker:Boolean;
	
	private var m_elevationpresent:Boolean;
	

	public function GISPoint(lat:Number, lon:Number, elev:Number, time:Number)
	{
		this.m_latitude = lat;
		this.m_longitude = lon;
		this.m_elevation = elev;
		if (elev)
			this.m_elevationpresent = true;
		else
			this.m_elevationpresent = false;			
		this.m_marker = false;
		this.m_time = time;
	}	
	
	public function getElev():Number
	{
		return this.m_elevation;
	}

	public function setElev(elevation:Number):void
	{
		this.m_elevation = elevation;
	}

	public function getLat():Number
	{
		return this.m_latitude;
	}
	public function getLatRad():Number
	{
		return toRad(this.m_latitude);
	}
	

	public function setLat(latitude:Number):void
	{
		this.m_latitude = latitude;
	}

	public function getLon():Number
	{
		return this.m_longitude;
	}
	public function getLonRad():Number
	{
		return toRad(this.m_longitude);
	}
	

	public function setLon(longitude:Number):void
	{
		this.m_longitude = longitude;
	}
	
	public function setTime(time:Number):void
	{
		this.m_time = time;
	}
	public function getTime() : Number
	{
		return this.m_time;
	}
	
	
	public function setComment(c:String):void
	{
		this.m_comment = c;
	}
	public function getComment() : String
	{
		return this.m_comment;
	}
	
	public function setCommentType(c:String):void
	{
		this.m_commenttype = c;
	}
	public function getCommentType() : String
	{
		return this.m_commenttype;
	}
	
	public function isMarker():Boolean
	{
		return this.m_marker;
	}
	public function setMarker(b:Boolean):void
	{
		this.m_marker = b;
	}
	
	/**
	 * Use Haversine formula to Calculate distance (in m) between two points specified by 
	 * GLatLng objects
	 *
	 * example usage from form:
	 *   result.value = GeoHelp.distHaversine(from, to);
	 *
	 * where from and to are GLatLng objects
	 */
	public function distance(to:GISPoint, useslope:Boolean) : Number
	{
		var from:GISPoint = this;
		var dLat:Number = (to.getLatRad() - from.getLatRad());
		var dLon:Number = (to.getLonRad() - from.getLonRad());		
		var latfrom:Number = from.getLatRad();
		var latto:Number = to.getLatRad();

		var a:Number =  Math.sin(dLat/2) * Math.sin(dLat/2) +
	          		Math.cos(latfrom) * Math.cos(latto) * 
	          		Math.sin(dLon/2) * Math.sin(dLon/2);
		var c:Number = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
		var d:Number= EARTH_RADIUS * c;
		
		// If there's elevation data and elevations are different
		// we must calculate the distance having into account the
		// slope (hypotenuse) reald = sqrt(dist^2 + elev^2)		
		if (useslope && from.hasElevation() && to.hasElevation()
			&& from.getElev()!=to.getElev())
		{
			var dElev:Number = to.getElev() - from.getElev();
			d = Math.sqrt(d*d + dElev*dElev);
		}
		return d;
	}

	public function hasElevation():Boolean
	{		
		return this.m_elevationpresent;
	}

	/**
	 * Calculates slope in % between two elevation points 
	 * @param currentpoint
	 * @return
	 */
	public function slope(to:GISPoint):Number
	{		
		var slope:Number = 0.0;
		slope = ((to.getElev() - this.getElev()) / this.distance(to, false));		
		return slope * 100;
	}
	
	public function travelTo( topoint:GISPoint, distance:Number, speed:Number):GISPoint
	{
		if (distance!=0 && speed!=0)
		{
			var dist:Number = distance/EARTH_RADIUS;
			var brng:Number = this.toRad(this.bearingTo(topoint));
			var lat1:Number = this.getLatRad();
			var lon1:Number = this.getLonRad();
			var lat2:Number = Math.asin(Math.sin(lat1)*Math.cos(dist) + Math.cos(lat1)*Math.sin(dist)*Math.cos(brng))
			var lon2:Number = lon1 + Math.atan2(Math.sin(brng)*Math.sin(dist)*Math.cos(lat1), Math.cos(dist)-Math.sin(lat1)*Math.sin(lat2));		
			lon2 = (lon2+Math.PI)%(2*Math.PI) - Math.PI;

			return new GISPoint(this.toDeg(lat2), 
								this.toDeg(lon2), 
								this.getElev() + (this.slope(topoint)/100) * distance,
								this.getTime() + (distance*1000 / speed));
		}
		else 
			return this;
		
	}
	
	public function copy():GISPoint
	{
		return new GISPoint(this.getLat(), this.getLon(), this.getElev(), this.getTime());
	}
	
	public function bearingTo(to:GISPoint):int
	{
		var from:GISPoint = this;
		var dLat:Number = (to.getLatRad() - from.getLatRad());
		var dLon:Number = (to.getLonRad() - from.getLonRad());		
		var latfrom:Number = from.getLatRad();
		var latto:Number = to.getLatRad();

		var y:Number = Math.sin(dLon) * Math.cos(latto);
		var x:Number = Math.cos(latfrom)*Math.sin(latto) - Math.sin(latfrom)*Math.cos(latto)*Math.cos(dLon);
		var brng:int = toBrng(Math.atan2(y, x));
		
		return brng;
	}
	
	// Computes speed in Km/h
	public function speed(to:GISPoint):Number
    {
          var speed:Number
          if (this.getTime() && to.getTime())
          {
              var timeDelta:Number = to.getTime() - this.getTime()
              if (timeDelta!=0)
              {
                  speed = (this.distance(to, true) / 1000) / (timeDelta / (1000*60*60))
              }
              else
              {
                  speed = 0 // No change in time means no speed
              }
          }
          return speed
    }
    
    // Computes vertical speed in m/s
    public function vspeed(to:GISPoint):Number
    {
    	var speed:Number
          if (this.getTime() && to.getTime())
          {
              var timeDelta:Number = to.getTime() - this.getTime()
              if (timeDelta!=0)
              {
                  speed = (to.getElev() - this.getElev()) / (timeDelta / 1000)
              }
              else
              {
                  speed = 0 // No change in time means no speed
              }
          }
          return speed
    }

	//	 extend String object with method for parsing degrees or lat/long values to numeric degrees
	//
	//	 this is very flexible on formats, allowing signed decimal degrees, or deg-min-sec suffixed by 
	//	 compass direction (NSEW). A variety of separators are accepted (eg 3º 37' 09"W) or fixed-width 
	//	 format without separators (eg 0033709W). Seconds and minutes may be omitted. (Minimal validation 
	//	 is done).

/*	private static function parseDeg(degrees:String) 
	{	 
		var deg:Number = 0.0;
		var degLL:String = degrees.replaceAll("^-", "").replaceAll("[NSEW]","");  // strip off any sign or compass dir'n
		degLL = degLL.trim();
		String dms[] = degLL.split("[^0-9.]+");                     // split out separate d/m/s
		for (int i=0; i<dms.length; i++)
			switch (dms.length) 
			{                                  // convert to decimal degrees...
			case 3:                                              // interpret 3-part result as d/m/s
				deg = 	Integer.parseInt(dms[0])/1 + 
						Integer.parseInt(dms[1])/60 + 
						Integer.parseInt(dms[2])/3600; 
				break;
			case 2:                                              // interpret 2-part result as d/m
				deg =   Integer.parseInt(dms[0])/1 + 
						Integer.parseInt(dms[1])/60; 
				break;
			case 1:                                              // decimal or non-separated dddmmss
				if (degrees.contains("NS"))
					degLL = '0' + degLL;       // - normalise N/S to 3-digit degrees
				deg = Integer.parseInt(dms[0].substring(0,3))/1 + 
						Integer.parseInt(dms[0].substring(3,5))/60 +
						Integer.parseInt(dms[0].substring(5))/3600;
				break;				
			}
		if (degrees.contains("-") || degrees.contains("WS")) 
			deg = -deg; // take '-', west and south as -ve
		return deg;
	}
*/
	

	private function toRad(val:Number):Number
	{  // convert degrees to radians
	  return val * Math.PI / 180.0;
	}

	private function toDeg(val:Number):Number
	{  // convert radians to degrees (signed)
	  return val * 180.0 / Math.PI;
	}

	private function toBrng(val:Number):int
	{  // convert radians to degrees (as bearing: 0...360)
	  return (int)(toDeg(val)+360) % 360;
	}

	/**
	 * Returns true if this point has the same latitude and longitude.
	 * 
	 * @param p2
	 * @return
	 */
	public function equals(p2:GISPoint):Boolean
	{
		if (this.getLat() == p2.getLat() &&
			this.getLon() == p2.getLon())
			return true;
		else
			return false;
	}
	
	/**
	 * Returns true if the two points are within the distance especified.
	 * 
	 * @param p2
	 * @param distance
	 * @return
	 */
	public function near(p2:GISPoint, distance:Number):Boolean
	{
		if (this.distance(p2, false) <= distance)
			return true;
		else
			return false;
	}
	
	public function toString():String
	{
		return "GISPoint: lat="+this.m_latitude+" lon="+this.m_longitude+" elev="+this.m_elevation+" time="+this.m_time;
	}
	
	 /**     method for parsing degrees or lat/long values to numeric degrees
     *
     *     this is very flexible on formats, allowing signed decimal degrees, or deg-min-sec suffixed by
     *     compass direction (NSEW). A variety of separators are accepted (eg 3º 37' 09"W)
     *  Seconds and minutes may be omitted. (Minimal validation
     *     is done)
     */
     public static function parseDeg(degrees:String):Number
     {
       var deg:Number = 0.0;
       var degLL:String;
       degLL = StringUtil.trim(degrees);
       var pattern1:RegExp = /^-?\d{1,3}\.\d+$/;
       var pattern2:RegExp = /\d{4,5}\.\d+[NSEW]$/;
       if (pattern1.test(degLL))   // Signed degrees ddd.dddd or dd.dddd
       {
         deg = Number(degLL);
       }
       else if(pattern2.test(degLL)) // Compass deg+minutes (NMEA). ddmm.mmmmW or dddmm.mmmmN
       {
       	 var pt:RegExp = /^(\d{2}|\d{3})(\d{2}\.\d+)[NSEW]$/;
       	 var d:Array = pt.exec(degLL);
       	 deg = Number(d[1]) + Number(d[2])/60;    
       }
       else
       {
         deg = null
       }
       var pattern3:RegExp = /.+[WS]$/;
       if (deg && pattern3.test(degLL))
           deg *= -1 // take west and south as -ve

       return deg;
     }


}

}